/* License Notice:
**
** This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
** This program is distributed in the hope that it will be useful,
**   but WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
**   GNU General Public License for more details.
** You should have received a copy of the GNU General Public License
**   along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/**
 * @file texture.cpp
 * @author TooOld2Rock'nRoll
 * @see texture.hpp
 */
/*---- Includes ----*/
#include "graphics/texture.hpp"

#include "toolbox/debug.h"
#include "config.h" //need to be here to limit the image file formats that stb_image is supposed to handle!

#define STB_IMAGE_IMPLEMENTATION
#include "toolbox/stb/stb_image.h"


/*---- Global Variables ----*/
//init static members
volatile TextureID_t Texture::last_texture = 0;
FrameBuffObj_t Texture::FBO[2] = {0, 0};
std::mutex Texture::mtx_blit_semaphore;


/*---- Methods Implementation ----*/
/**
 * @brief Base Constructor.
 *
 * Generates a OpenGl <i>TextureID_t</i> and set some basic parameter to make it render properly.
 */
Texture::Texture ()
{
    //sets minimal options to correctly render a texture
    //https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexParameter.xhtml

    _debug (TRACE, "::%s ()", __FUNCTION__);

    glGenTextures (1, &this->ID);

    this->bind ();

    //The texture minifying function is used whenever the level-of-detail function used when sampling from the
    //   texture determines that the texture should be minified.
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    //The texture magnification function is used whenever the level-of-detail function used when sampling from
    //   the texture determines that the texture should be magnified.
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    //How to fill the area around a texture.
    //We chouse to clamp to border as to keep the images as is without any blending effect.
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); //-S means -U that is x axle
    glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER); //-T means -V that is y axle
}//End Texture Constructor

/**
 * @brief Extended Constructor that loads a image file.
 *
 * Generates a OpenGl <i>TextureID_t</i>, set some basic parameter to make it render properly and load a image file as texture.
 *
 * @param file_path - path to the image file to be used as texture.
 *
 * @throws std::runtime_error - in case there is any problem loading the image file.
 */
Texture::Texture (const char *file_path) : Texture ()
{
    _debug (TRACE, "::%s (file_path=%s)", __FUNCTION__, file_path);

    this->load (file_path);
}//End Texture Constructor

/**
 * @brief Destructor.
 */
Texture::~Texture ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    glDeleteTextures (1, &this->ID);

    if (this->pa_texture_map)
        delete[] this->pa_texture_map;
}//End Texture Destructor

/**
 * @brief Binds the texture to the texturing target.
 *
 * Before executing any operations to a texture, it must first be bound.<br>
 * All methods in this class execute bind when necessary so you don't have to.
 *
 * @see https://www.khronos.org/opengl/wiki/GLAPI/glBindTexture
 */
void Texture::bind ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    //Only binds if not bound yet
    if (Texture::last_texture == this->ID)
        return;

    glBindTexture (GL_TEXTURE_2D, this->ID);
    Texture::last_texture = this->ID;
}//End bind ()

/**
 * @brief Tries to load a image file and set it as a texture.
 *
 * @param file_path - path to the image file to be used as texture.
 *
 * @throws std::runtime_error - in case there is any problem loading the image file.
 *
 * @see stb_image.h
 */
void Texture::load (const char *file_path)
{
    int width = 0,
        height = 0,
        numChannels = 0;
    uint8_t *data = nullptr;

    _debug (TRACE, "::%s (file_path=%s)", __FUNCTION__, file_path);

    data = stbi_load (file_path, &width, &height, &numChannels, 0);
    if (!data)
        throw std::runtime_error (fmt::format ("{0}:{1} - Failed to load: {2}!", __LINE__, __FILE__, file_path));

    this->load (data, width, height, numChannels);

    stbi_image_free (data);
}//End load (file)

/**
 * @brief Load a data array as texture.
 *
 * Number of channels supported are:<br>
 * 1 - gray scale.<br>
 * 2 - gray scale with alpha.<br>
 * 3 - RGB.<br>
 * 4 - RGB with Alpha<br>
 *
 * @param data - pointer to the image data.
 * @param width - of the image in pixels.
 * @param height - of the image in pixels.
 * @param channels - number of channels in the image [1|2|3|4].
 *
 * @see https://registry.khronos.org/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml
 */
void Texture::load (unsigned char *data, unsigned width, unsigned height, unsigned channels)
{
    GLint t_format = 0;

    _debug (TRACE, "::%s (with data array)", __FUNCTION__);

    if (this->b_loaded)
        _debug (WARNING, "This Texture already have an image loaded, are you sure you want to overwrite it?");

    this->u_width = width;
    this->u_height = height;

    this->bind ();

    switch (channels)
    {
        case 1: //grey scale
            t_format = GL_RED;
            glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_RED);
            glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
            if (this->b_mono_black_as_alpha)
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_RED);

            glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
            break;

        case 2: //grey scale with alpha
            t_format = GL_RG;
            glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, GL_RED);
            glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
            glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_GREEN);
            glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
            break;

        case 3:
            t_format = GL_RGB;
            glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
            break;

        case 4:
            t_format = GL_RGBA;
            break;

        default:
            t_format = GL_RGB;
    }//end switch (nrChannels)

    glTexImage2D (GL_TEXTURE_2D, 0, t_format, width, height, 0, t_format, GL_UNSIGNED_BYTE, data);

    this->b_loaded = true;
}//End load (buffer)


/**
 * @brief Set images to be flipped.
 * @warning Global method action, it will be active until turned off again.
 *
 * If you are using the default OpenGl screen orientation, textures will be upside down and you may want to flip
 *   them automatically during loading.
 *
 * @param flip - set to <b>true</b> if all images should be flipped on the vertical axle on load.
 */
void Texture::flipOnLoad (bool flip)
{
    _debug (TRACE, "::%s (flip=%s)", __FUNCTION__, (flip ? "true" : "false"));

    stbi_set_flip_vertically_on_load (flip);
}//End flipOnLoad ()


/**
 * @brief Creates a <b>empty</b> tile map, so its tiles may have UV coordinates for cropping the texture.
 *
 * @param tile_count - the number of tiles present in the Texture.
 *
 * @return A reference to the new empty tile map array with <i>tile_count</i> entries in the Texture.
 *
 * @throws std::invalid_argument - if tile_count is less then two (it makes no sense!).
 * @throws std::runtime_error - if a map was already allocated previously.
 */
const Texture::tile_uv& Texture::createTileMap (unsigned tile_count)
{
    _debug (TRACE, "::%s (tile_count=%u)", __FUNCTION__, tile_count);

    if (tile_count < 2)
        throw std::invalid_argument (fmt::format ("{0}:{1} - tile count must be >= 2!", __LINE__, __FILE__));
    else if (this->pa_texture_map)
        throw std::runtime_error (fmt::format ("{0}:{1} - Texture already has a texture map allocated!", __LINE__, __FILE__));

    this->u_tile_count = tile_count;
    this->pa_texture_map = new Texture::tile_uv[tile_count];

    return *this->pa_texture_map;
}//End createTileMap ()


/**
 * @brief Creates a tile map with <i>columns * rows</i> tiles and maps all the tiles.
 *
 * This is the method to use if your texture has contiguous and uniform sized tiles.
 *
 * @param columns - number of horizontal tiles.
 * @param rows - number of vertical tiles.
 * @param padding - reference to an array with length of 4 representing how much to shrink the tiles at [l,w,t,b]
 *
 * @return A reference to the new tile map array with <i>columns * rows</i> entries and all tiles limits processed.
 *
 * @throws std::invalid_argument - if <i>columns * rows</i> is less than two (it makes no sense!).
 * @throws std::runtime_error - if a map was already allocated previously.
 */
const Texture::tile_uv& Texture::createTileMap (unsigned columns, unsigned rows, const unsigned (&padding)[4])
{
    _debug (TRACE, "::%s (cols=%u, rows=%u)", __FUNCTION__, columns, rows);

    this->createTileMap (columns * rows);
    this->autoMapTiles (columns, rows, padding);

    return *this->pa_texture_map;
}//End createTileMap ()


/**
 * @brief Gets a reference to a specific tile's information.
 *
 * @param index - the tile index.
 *
 * @return A reference to the desired tile information.
 *
 * @throws std::runtime_error - if the texture has no tile map.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
const Texture::tile_uv& Texture::getTileLimits (unsigned index) const
{
    _debug (TRACE, "::%s (index=%u)", __FUNCTION__, index);

    if (!this->pa_texture_map)
        throw std::runtime_error (fmt::format ("{0}:{1} - Texture has no tile map allocated!", __LINE__, __FILE__));
    else if (index >= this->u_tile_count)
        throw std::out_of_range (fmt::format ("{0}:{1} - Tile out of range, {2} of {3}!", __LINE__, __FILE__, index, this->u_tile_count - 1));

    return this->pa_texture_map[index];
}//end getTileLimits ()


/**
 * @brief Automatically maps the limits of all tiles in the texture using the number of tiles per columns/rows.
 *
 * If the tiles in the texture are uniform in size and contiguous, this method
 *    can be used to automatically map the position of them all.
 *
 * @param columns - number of horizontal tiles.
 * @param rows - number of vertical tiles.
 * @param padding - reference to an array with length of 4 representing how much to shrink the tiles at [l,w,t,b]
 *
 * @throws std::runtime_error - if the texture has no tile map yet.
 * @throws std::out_of_range - if the created map has less entries than {columns * rows}.
 */
void Texture::autoMapTiles (unsigned columns, unsigned rows, const unsigned (&padding)[4])
{
    _debug (TRACE, "::%s (rows=%u, columns=%u)", __FUNCTION__, rows, columns);

    if (columns * rows > this->u_tile_count)
        throw std::out_of_range (fmt::format ("{0}:{1} - Tile count out of range, {2} of {3}!", __LINE__, __FILE__, columns * rows , this->u_tile_count));

    this->autoMapTiles (0, 0, this->u_width/columns, this->u_height/rows, padding);
}//End autoMapTiles ()

/**
 * @brief Automatically maps the limits of all tiles in the texture using a start point and the size of the tiles in pixels.
 *
 * If the tiles in the texture are uniform in size and contiguous, this method
 *    can be used to automatically map the position of all tiles.
 *
 * @param x_start - x position of the top left corner of the first tile.
 * @param y_start - y position of the top left corner of the first tile.
 * @param tile_w - width in pixels of the tiles.
 * @param tile_h - height in pixels of the times.
 * @param padding - reference to an array with length of 4 representing how much to shrink the tiles at [l,w,t,b]
 *
 * @throws std::runtime_error - if the texture has no tile map created yet.
 */
void Texture::autoMapTiles (unsigned x_start, unsigned y_start, unsigned tile_w, unsigned tile_h, const unsigned (&padding)[4])
{
    unsigned i = 0;

    _debug (TRACE, "::%s (x=%u, y=%u, w=%u, h=%u)", __FUNCTION__, x_start, y_start, tile_w, tile_h);

    //map
    for ( ; i < this->u_tile_count; ++i)
    {
        this->mapTileBySize (i, x_start, y_start, tile_w, tile_h, padding);

        if (x_start + tile_w < this->u_width)
            x_start += tile_w; //go to next tile
        else
        {
            //go to next line!
            x_start = 0;
            y_start += tile_h;
        }//end else
    }//end for ( ; i < tile_count; ++i)
}//End autoMapTiles ()


/**
 * @brief Saves the coordinates of a tile limits using its absolute positions.
 *
 * @param tile_index - the tile index.
 * @param x1 - left limit in pixels.
 * @param y1 - top limit in pixels.
 * @param x2 - right limit in pixels.
 * @param y2 - bottom limit in pixels.
 * @param padding - reference to an array with length of 4 representing how much to shrink the tiles at [l,w,t,b]
 *
 * @throws std::runtime_error - if the texture has no tile map created yet.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Texture::mapTileByLimits (unsigned tile_index, unsigned x1, unsigned y1, unsigned x2, unsigned y2, const unsigned (&padding)[4])
{
    _debug (TRACE, "::%s (index=%u, limits={%u, %u, %u, %u})", __FUNCTION__, tile_index, x1, y1, x2, y2);

    if (!this->pa_texture_map)
        throw std::runtime_error (fmt::format ("{0}:{1} - Texture has no tile map allocated!", __LINE__, __FILE__));
    else if (tile_index >= this->u_tile_count)
        throw std::out_of_range (fmt::format ("{0}:{1} - Tile out of range, {2} of {3}!", __LINE__, __FILE__, tile_index, this->u_tile_count - 1));

    x1 += padding[0];
    x2 -= padding[1];
    y1 += padding[2];
    y2 -= padding[3];

    this->pa_texture_map[tile_index].u_width = x2-x1;
    this->pa_texture_map[tile_index].u_height = y2-y1;

    this->pa_texture_map[tile_index].u_x1 = x1;
    this->pa_texture_map[tile_index].u_y1 = y1;
    this->pa_texture_map[tile_index].u_x2 = x2;
    this->pa_texture_map[tile_index].u_y2 = y2;

    this->pa_texture_map[tile_index].f_x1 = pixel2glmCoordinate (this->u_width, x1);
    this->pa_texture_map[tile_index].f_y1 = pixel2glmCoordinate (this->u_height, y1);
    this->pa_texture_map[tile_index].f_x2 = pixel2glmCoordinate (this->u_width, x2);
    this->pa_texture_map[tile_index].f_y2 = pixel2glmCoordinate (this->u_height, y2);
}//End mapTileByLimits ()


/**
 * @brief Saves the coordinates of a tile limits using its size.
 *
 * @param tile_index - the tile index.
 * @param x - left position in pixels.
 * @param y - top position in pixels.
 * @param w - width in pixels.
 * @param h - height in pixels.
 * @param padding - reference to an array with length of 4 representing how much to shrink the tiles at [l,w,t,b]
 *
 * @throws std::runtime_error - if the texture has no tile map created yet.
 * @throws std::out_of_range - if the tile index is greater than the max number of tiles supported.
 */
void Texture::mapTileBySize (unsigned tile_index, unsigned x, unsigned y, unsigned w, unsigned h, const unsigned (&padding)[4])
{
    _debug (TRACE, "::%s (index=%u, tile={%u, %u, %u, %u})", __FUNCTION__, tile_index, x, y, w, h);

    this->mapTileByLimits (tile_index, x, y, x+w, y+h, padding);
}//End mapTileBySize ()


/**
 * @brief Setup the necessary procedures to execute a Blit operation over two Textures.
 * @remarks Only one Blit operation may be in execution at any given time, a semaphore is used to block any parallel
 *            attempt to such behavior.
 *
 * @param source_id - The Texture that will be used as source to the Blit operation.
 * @param target_id - The Texture that will be used as target to the Blit operation.
 */
void Texture::blitSet (TextureID_t source_id, TextureID_t target_id)
{
    _debug (TRACE, "::%s (src=%u, dst=%u)", __FUNCTION__, source_id, target_id);

    Texture::mtx_blit_semaphore.lock ();
    _debug (INFO, "Blit semaphore was locked, remember to call Texture::blitEnd() to unlock it!");

    //prepare the frame buffers
    glGenFramebuffers (2, Texture::FBO);

    glBindFramebuffer (GL_READ_FRAMEBUFFER, FBO[0]);
    glFramebufferTexture2D (GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, source_id, 0);
    if (glCheckFramebufferStatus (GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        _debug (ERROR, "Read FrameBuffer is incomplete, probably something wrong with the source Texture!");

    glBindFramebuffer (GL_DRAW_FRAMEBUFFER, FBO[1]);
    glFramebufferTexture2D (GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target_id, 0);
    if (glCheckFramebufferStatus (GL_DRAW_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
        _debug (ERROR, "Draw FrameBuffer is incomplete, probably something wrong with the target Texture!");
}//End blitSet ()

/**
 * @brief Combine sections of two Textures.
 *
 * @param src_x - The x coordinate in pixels of the source Texture.
 * @param src_y - The y coordinate in pixels of the source Texture.
 * @param dst_x - The x coordinate in pixels of the destination Texture.
 * @param dst_y - The y coordinate in pixels of the destination Texture.
 * @param w - the width of the area to be copied in pixels.
 * @param h - the height of the area to be copied in pixels.
 * @param mask - a OpenGl bit wise mask to be applied on the Textures.
 * @param filter - a OpenGl filter to be used on the target.
 *
 * @see https://registry.khronos.org/OpenGL-Refpages/gl4/html/glBlitFramebuffer.xhtml
 */
void Texture::blit (int src_x, int src_y, int dst_x, int dst_y, unsigned w, unsigned h, GLbitfield mask, GLenum filter)
{
    _debug (VERBOSE, "::%s ()", __FUNCTION__);

    glBlitFramebuffer (src_x, src_y, src_x + w, src_y + h,
                       dst_x, dst_y, dst_x + w, dst_y + h,
                       GL_COLOR_BUFFER_BIT, GL_NEAREST);
}//End blit ()

/**
 * @brief Clears the necessary structures to end the Blit operation.
 */
void Texture::blitEnd ()
{
    _debug (TRACE, "::%s ()", __FUNCTION__);

    Texture::mtx_blit_semaphore.unlock ();
    _debug (INFO, "Blit semaphore was unlocked!");

    glBindFramebuffer (GL_READ_FRAMEBUFFER, 0); // Disable FBO when done
    glBindFramebuffer (GL_DRAW_FRAMEBUFFER, 0); // Disable FBO when done
    glDeleteFramebuffers (2, Texture::FBO);
}//End blitEnd ()

